SPB Git
6.3 KB · 77 lines tsx
1"use client";2import { useEffect, useState } from "react";3import { useParams, useRouter } from "next/navigation";4import useSWR, { mutate as gmutate } from "swr";5import { FolderKanban, Archive, Trash2, Check, AlertTriangle } from "lucide-react";6import { api, fetcher } from "@/lib/client/fetcher";7import type { ArtifactDTO, MemoryDTO, ProjectDTO } from "@/lib/client/types";8import { Button, Spinner, Tabs, cn, toast } from "@/components/ui";9import { ColorPicker } from "@/components/projects/project-shared";10import { ArtifactsTab, ConversationsTab, FilesTab, InstructionsTab, MemoryTab, NotesTab, type ProjectFile } from "@/components/projects/project-tabs";1112interface Resp { project: ProjectDTO; files: ProjectFile[]; memories: MemoryDTO[]; artifacts: ArtifactDTO[] }13type Tab = "conversations" | "instructions" | "files" | "memory" | "notes" | "artifacts";1415export default function ProjectPage() {16  const { id } = useParams<{ id: string }>(); const router = useRouter();17  const { data, error, mutate } = useSWR<Resp>(`/api/projects/${id}`, fetcher);18  const [tab, setTab] = useState<Tab>("conversations");19  const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [editingHead, setEditingHead] = useState(false);20  useEffect(() => { if (data) { setName(data.project.name); setDescription(data.project.description ?? ""); } }, [data]);21  const refresh = () => { void mutate(); void gmutate("/api/projects"); };22  async function patch(p: Record<string, unknown>, msg?: string) { try { await api(`/api/projects/${id}`, { method: "PATCH", json: p }); if (msg) toast(msg, "ok"); refresh(); } catch (e) { toast((e as Error).message, "error"); } }23  async function saveHead() { await patch({ name: name.trim() || data?.project.name, description: description.trim() || null }, "Projet mis à jour"); setEditingHead(false); }24  async function archive() { if (!confirm("Archiver ce projet ? Ses conversations restent accessibles dans l'historique.")) return; await patch({ archived: true }, "Projet archivé"); router.push("/"); }25  async function del() { if (!confirm("Supprimer définitivement ce projet ? Les conversations sont conservées mais détachées ; les fichiers et souvenirs du projet sont supprimés.")) return; await api(`/api/projects/${id}`, { method: "DELETE" }); void gmutate("/api/projects"); toast("Projet supprimé", "ok"); router.push("/"); }2627  if (error) return <div className="p-8 text-sm text-danger">Projet introuvable.</div>;28  if (!data) return <div className="flex flex-1 items-center justify-center"><Spinner className="h-5 w-5" /></div>;29  const p = data.project;30  const tabs: { value: Tab; label: string; count?: number }[] = [31    { value: "conversations", label: "Conversations", count: p.conversation_count }, { value: "instructions", label: "Instructions" }, { value: "files", label: "Fichiers", count: data.files.length },32    { value: "memory", label: "Mémoire", count: data.memories.length }, { value: "notes", label: "Notes" }, { value: "artifacts", label: "Artefacts", count: data.artifacts.length },33  ];34  return (35    <div className="min-h-0 flex-1 overflow-y-auto">36      <div className="mx-auto w-full max-w-4xl px-4 py-6 md:px-8">37        <div className="mb-5 flex flex-wrap items-start gap-3">38          <span className="mt-1.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl" style={{ background: `${p.color ?? "#6d9cff"}22`, color: p.color ?? "var(--accent)" }}><FolderKanban className="h-5 w-5" /></span>39          <div className="min-w-0 flex-1">40            {editingHead ? (41              <div className="space-y-2">42                <input value={name} onChange={e => setName(e.target.value)} maxLength={120} className="input text-lg font-semibold" autoFocus />43                <input value={description} onChange={e => setDescription(e.target.value)} maxLength={2000} className="input text-[13px]" placeholder="Description" />44                <div className="flex items-center gap-3"><ColorPicker value={p.color} onChange={c => patch({ color: c })} /><span className="flex-1" /><Button variant="ghost" size="sm" onClick={() => setEditingHead(false)}>Annuler</Button><Button variant="primary" size="sm" onClick={saveHead}><Check className="h-3.5 w-3.5" /> Enregistrer</Button></div>45              </div>46            ) : (47              <button onClick={() => setEditingHead(true)} className="group block w-full text-left" title="Modifier le nom, la description et la couleur">48                <h1 className="truncate text-xl font-semibold tracking-tight group-hover:text-accent">{p.name}</h1>49                <p className="mt-0.5 text-[13px] text-muted">{p.description || "Ajouter une description…"}</p>50              </button>51            )}52          </div>53        </div>5455        <div className="mb-5 -mx-1 overflow-x-auto px-1">56          <Tabs value={tab} onChange={setTab} items={tabs.map(t => ({ value: t.value, label: <span>{t.label}{t.count != null ? <span className="ml-1 text-muted">{t.count}</span> : null}</span> }))} />57        </div>5859        <div className={cn("anim-fade-up")} key={tab}>60          {tab === "conversations" && <ConversationsTab project={p} />}61          {tab === "instructions" && <InstructionsTab project={p} onSaved={refresh} />}62          {tab === "files" && <FilesTab project={p} files={data.files} onChanged={refresh} />}63          {tab === "memory" && <MemoryTab project={p} memories={data.memories} onChanged={refresh} />}64          {tab === "notes" && <NotesTab project={p} />}65          {tab === "artifacts" && <ArtifactsTab artifacts={data.artifacts} />}66        </div>6768        <section className="mt-12 rounded-2xl border border-danger/30 p-5">69          <h2 className="mb-1 flex items-center gap-2 text-[13px] font-semibold uppercase tracking-wider text-danger"><AlertTriangle className="h-4 w-4" /> Zone sensible</h2>70          <p className="mb-3 text-[13px] text-muted">Archiver masque le projet de la barre latérale sans rien supprimer. Supprimer efface les fichiers, notes et souvenirs du projet ; les conversations sont conservées sans projet.</p>71          <div className="flex flex-wrap gap-2"><Button variant="outline" onClick={archive}><Archive className="h-4 w-4" /> Archiver</Button><Button variant="danger" onClick={del}><Trash2 className="h-4 w-4" /> Supprimer le projet</Button></div>72        </section>73      </div>74    </div>75  );76}77
Files · main
Menu

Theme